{-#LANGUAGE FlexibleContexts#-}
{-#LANGUAGE RankNTypes#-}

module Time
  ( getCurrentLocalTime
  , readDayP
  , readTimeOfDayP
  , readTimeP
  , maybeTimeP
  , showTime
  , showMaybeTime
  , timeEditField
  ) where

import qualified Data.Time as Time
import qualified Data.Text as Text
import Data.Functor.Identity (Identity)
import qualified Text.Parsec as P
import qualified Brick.Widgets.Edit as E
import Brick.Widgets.Core
  ( txt
  , str
  , vBox
  )
import Brick.Forms
  ( FormFieldState(..)
  , FormField(..)
  , FormFieldVisibilityMode(..)
  )
import Lens.Micro
  ( Lens'
  , (^.)
  )

getCurrentLocalTime :: IO Time.LocalTime
getCurrentLocalTime = do
  z <- Time.getCurrentTimeZone
  t <- Time.getCurrentTime
  return $ Time.utcToLocalTime z t

intP :: (Read i, P.Stream s Identity Char)
     => P.Parsec s u i
intP = read <$> P.many1 P.digit

dayValidateP :: P.Stream s Identity Char
             => Time.Year
             -> Time.MonthOfYear
             -> Time.DayOfMonth
             -> P.Parsec s u Time.Day
dayValidateP y m d =
  case Time.fromGregorianValid y m d of
    Nothing -> P.unexpected "invalid day format"
    Just day -> return day

-- | Parse the day of month
dayP :: P.Stream s Identity Char
     => (Time.Year, Time.MonthOfYear)
     -> P.Parsec s u Time.Day
dayP (y, m) = do
  d <- intP
  dayValidateP y m d

monthDayP :: P.Stream s Identity Char
          => Time.Year
          -> P.Parsec s u Time.Day
monthDayP y = do
  m <- intP
  _ <- P.char '-'
  d <- intP
  dayValidateP y m d

yearMonthDayP :: P.Stream s Identity Char
              => P.Parsec s u Time.Day
yearMonthDayP = do
  y <- intP
  _ <- P.char '-'
  m <- intP
  _ <- P.char '-'
  d <- intP
  dayValidateP y m d

readDayP :: P.Stream s Identity Char
         => Time.Day
         -- ^ Current day
         -> P.Parsec s u Time.Day
readDayP day = P.choice [ P.try $ yearMonthDayP
                        , P.try $ monthDayP y
                        , P.try $ dayP (y, m)
                        ] where
  (y, m, _) = Time.toGregorian day

timeValidate :: Time.TimeOfDay
             -> Bool
timeValidate (Time.TimeOfDay h m s) =
  h >= 0 && h < 24 &&
  m >= 0 && m < 60 &&
  s >= 0 && s < 60

hourP :: P.Stream s Identity Char
      => P.Parsec s u Time.TimeOfDay
hourP = do
  h <- intP
  let td = Time.TimeOfDay h 0 0
  if timeValidate td
    then return td
    else P.unexpected "invalid time format"

hourMinuteP :: P.Stream s Identity Char
            => P.Parsec s u Time.TimeOfDay
hourMinuteP = do
  h <- intP
  _ <- P.char ':'
  m <- intP
  let td = Time.TimeOfDay h m 0
  if timeValidate td
    then return td
    else P.unexpected "invalid time format"

hourMinuteSecondP :: P.Stream s Identity Char
                  => P.Parsec s u Time.TimeOfDay
hourMinuteSecondP = do
  h <- intP
  _ <- P.char ':'
  m <- intP
  _ <- P.char ':'
  s <- intP
  let td = Time.TimeOfDay h m s
  if timeValidate td
    then return td
    else P.unexpected "invalid time format"

readTimeOfDayP :: P.Stream s Identity Char
               => P.Parsec s u Time.TimeOfDay
readTimeOfDayP = P.choice [ P.try $ hourMinuteSecondP
                          , P.try $ hourMinuteP
                          , P.try $ hourP
                          ]

-- | Create time read parser
readTimeP :: P.Stream s Identity Char
          => Time.LocalTime
          -- ^ Current time for default fields
          -> P.Parsec s u Time.LocalTime
          -- ^ Return time parser
readTimeP (Time.LocalTime d td) =
  P.choice [ P.try $ dayTimeP <* P.eof
           , P.try $ onlyDayP td <* P.eof
           , P.try $ onlyTimeP d <* P.eof
           ]
  where
  dayTimeP = do
    day <- readDayP d
    P.spaces
    timeOfDay <- readTimeOfDayP
    return $ Time.LocalTime day timeOfDay
  onlyDayP timeOfDay = do
    day <- readDayP d
    return $ Time.LocalTime day timeOfDay
  onlyTimeP day = do
    timeOfDay <- readTimeOfDayP
    return $ Time.LocalTime day timeOfDay

showTime :: Time.LocalTime -> Text.Text
showTime t = Text.pack $ show nt where
  timeOfDay = Time.localTimeOfDay t
  nt = t { Time.localTimeOfDay = timeOfDay { Time.todSec = ns } }
  ns = fromInteger . floor . Time.todSec $ timeOfDay

showMaybeTime :: Maybe Time.LocalTime -> Text.Text
showMaybeTime Nothing = Text.empty
showMaybeTime (Just t) = showTime t

maybeTimeP :: P.Stream s Identity Char
           => Time.LocalTime
           -> P.Parsec s u (Maybe Time.LocalTime)
maybeTimeP l = P.choice [ P.try unsetTimeP
                        , P.try setTimeP
                        ] where
  -- | empty text means we don't set time
  unsetTimeP = Nothing <$ P.spaces <* P.eof
  setTimeP = Just <$> readTimeP l

timeEditField :: (Ord n, Show n)
              => Lens' s (Maybe Time.LocalTime)
              -> n
              -> Time.LocalTime
              -> (Maybe Time.LocalTime -> Text.Text)
              -> s
              -> FormFieldState s e n
timeEditField l n c ini s =
  FormFieldState { formFieldState = initialEdit
                 , formFields = [ customField ]
                 , formFieldLens = l
                 , formFieldUpdate = update
                 , formFieldRenderHelper = id
                 , formFieldConcat = vBox
                 , formFieldVisibilityMode = ShowFocusedFieldOnly
                 } where
  initialText = ini $ s^.l
  initialEdit = E.editor n (Just 1) initialText
  update a _ = E.editor n (Just 1) (ini a) -- TODO update instead of create
  validate e =
    let line = Text.unpack $ Text.strip (E.getEditContents e !! 0)
    in case P.parse (maybeTimeP c) "time editor" line of
      Left _ -> Nothing
      Right mt -> Just mt
  renderEdit b e =
    let editor = E.renderEditor (txt . Text.unlines) b e
        line = Text.unpack $ Text.strip (E.getEditContents e !! 0)
        echo = case P.parse (maybeTimeP c) "time editor" line of
          Left _ -> str $ "^ parse failed"
          Right Nothing -> str $ "^ time unset"
          Right (Just st) -> str $ "^ " ++ Text.unpack (showTime st)
    in if True
       then vBox [ editor
                 , echo
                 ]
       else editor
  customField =
    FormField { formFieldName = n
              , formFieldValidate = validate
              , formFieldExternallyValid = True
              , formFieldRender = renderEdit
              , formFieldHandleEvent = E.handleEditorEvent
              }
